Skip to content

feat(ui5-toolbar): implement WAI-ARIA toolbar keyboard navigation#13622

Open
plamenivanov91 wants to merge 17 commits into
mainfrom
tb-arrow-nav-1-june
Open

feat(ui5-toolbar): implement WAI-ARIA toolbar keyboard navigation#13622
plamenivanov91 wants to merge 17 commits into
mainfrom
tb-arrow-nav-1-june

Conversation

@plamenivanov91

@plamenivanov91 plamenivanov91 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Replace Tab-through-every-item with a proper arrow-key navigation pattern
per the WAI-ARIA toolbar pattern.

Keyboard handling

  • [Left]/[Right] — navigate between toolbar items (RTL-aware); absorbed at the first/last item (no wrap)
  • [Home]/[End] — jump to first/last item, unless the focused item manages its own internal navigation (e.g. Input caret, Breadcrumbs), in which case the item handles the key
  • [Tab]/[Shift+Tab] — exit the toolbar via the browser's natural tab order; no roving tabindex, items keep their own natural tabindex
  • Overflow popover is fully isolated: arrow keys and focusin handling are suppressed while the popover is open. On open, the first interactive overflow item receives focus.

Focus tracking

_lastFocusedItem tracks the current navigation target for re-entry (e.g. Shift+Tab back into the toolbar). It never mutates tabindex and is reconciled after reflow — a tracked item that moves to overflow or is removed no longer causes navigation to restart from the first item. When the overflow button disappears (items move back into the toolbar), focus moves to the last navigable item via focusForToolbarNavigation.

Interactivity detection

An item counts as interactive (and toward role="toolbar") only when its resolved focus target is actually focusable:

  • Text-only content (ui5-text, ui5-title) is skipped
  • Focus-delegating hosts (ui5-date-picker) are resolved through getFocusDomRef and remain navigable
  • href-less anchors that opt in via an explicit tabindex (e.g. ui5-link rendered as a button, used by the Breadcrumbs overflow arrow) stay navigable

ToolbarItemBase — hook API for complex items

New overridable members (no-ops in base):

  • getArrowNavState() — reports { atLeftEnd, atRightEnd } so the toolbar knows when a key press is at an internal boundary and should be handled by the item rather than moving to the next item
  • focusForToolbarNavigation(isForward) — direction-aware focus entry point

ToolbarItem — three navigation paths

  • _itemNavigation introspection — components that use ItemNavigation internally (e.g. Breadcrumbs, SegmentedButton) are detected via duck-typing; currentIndex/itemCount derive the boundary state
  • getArrowNavState interface — explicit opt-in for components that expose caret/selection position as boundary info (e.g. Input, TextArea)
  • Multi-child fallbackToolbarItems with more than one slotted child (radio/checkbox groups) are treated as a navigable group; Left/Right and Home/End move within the group, exiting only at its boundary

Input / TextArea — caret-aware boundary

Both implement getArrowNavState() reporting atLeftEnd/atRightEnd from the caret position, and only when the selection is collapsed — a non-collapsed selection is not treated as a boundary, so Left/Right collapse it natively instead of exiting to the next toolbar item.

Accessibility docs

  • Updated keyboard-handling JSDoc on ui5-toolbar
  • Added WCAG 2.1 notes to accessibleName / accessibleNameRef

Tests (Toolbar.cy.tsx, ToolbarSelect.cy.tsx)

  • Arrow Left/Right navigation; no-wrap at the ends; Home/End to first/last
  • Tab traverses items via natural tab order
  • Input/TextArea caret-aware: Left/Right exits only at the caret boundary; a range selection at a boundary stays in the field
  • Home not intercepted by the toolbar when an Input has focus
  • RTL reverses Left/Right direction
  • Checkbox group: arrow traversal within the group, Home/End within the group, and boundary exit
  • Text-only items non-interactive; focus-delegating hosts interactive; self-overflowed Breadcrumbs stays navigable
  • First overflow item focused on popover open; overflow button ArrowLeft navigates to the last visible toolbar item
  • ToolbarSelect programmatic selection stabilised against the light-DOM option / rendered select value

JIRA: BGSOFUIPIRIN-7018
Fixes: #12945

…2 (prevent page scroll on unhandled up/down)
Refactor toolbar keyboard handling around a single toolbar-owned flow.

- centralize arrow and tab navigation in Toolbar
- add movement-info and roving-tabindex hooks to ToolbarItemBase
- adapt grouped ToolbarItem content through shared internal target logic
- restore caret-aware movement for Input and TextArea
- apply forced tabindex to toolbar button/select templates
- remove redundant select-owned keyboard handling
- add Toolbar regressions for checkbox groups and overflow-button exit
@ui5-webcomponents-bot

ui5-webcomponents-bot commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

@ui5-webcomponents-bot
ui5-webcomponents-bot temporarily deployed to preview June 2, 2026 08:48 Inactive
@ui5-webcomponents-bot
ui5-webcomponents-bot temporarily deployed to preview June 2, 2026 09:45 Inactive
- Skip focusin/keydown handling when focus is inside the open overflow
  popover, preventing arrow-nav logic from firing inside the popover
- Skip forcedTabIndex on overflowed ToolbarButton/ToolbarSelect so
  overflow items keep their natural tab order
- Fix Tab-exit containment check to use shadow-DOM-aware walk
  (_isNodeInsideElement) instead of contains/shadowRoot.contains
- Remove own-fallback movement info path from ToolbarItem; items without
  _itemNavigation or getToolbarMovementInfo are now treated as single
  tab stops
- Drop dead WeakMap tab-index restoration machinery (no longer needed
  now that overflow items manage their own tab order)
@ui5-webcomponents-bot
ui5-webcomponents-bot temporarily deployed to preview June 3, 2026 10:24 Inactive
@plamenivanov91 plamenivanov91 changed the title fix(ui5-toolbar): simplify toolbar item keyboard navigation feat(ui5-toolbar): implement WAI-ARIA toolbar keyboard navigation Jun 3, 2026
Comment thread packages/main/src/Toolbar.ts Outdated
Comment thread packages/main/src/ToolbarItem.ts Outdated
Comment thread packages/main/src/Toolbar.ts Outdated
Comment thread packages/main/src/RadioButton.ts
Comment thread packages/main/src/Toolbar.ts Outdated
@plamenivanov91
plamenivanov91 marked this pull request as ready for review June 10, 2026 12:02
@github-actions

Copy link
Copy Markdown

👋 Heads-up: dev close is in effect

Thanks for the contribution! This repository is currently in dev close ahead of release next (scheduled 2026-07-09, UTC). See the release schedule for the full timeline.

This PR appears to introduce public-API changes (detected by diffing the Custom Elements Manifest against the latest published version on npm):

@ui5/webcomponents

  • ➕ added interface: IInputSuggestionItem
  • ➕ added interface: IToolbarItemContent
  • 🔄 changed attribute value-state (type)
  • 🔄 changed attribute value-state (type)

Could you please hold off on merging into main until the release ships? Public-API changes are best landed in the next dev cycle so they don't slip into the release at the last minute. Once the release is out, this PR is good to go.

If this change must ship in the current release, please request a review from one or two members of @UI5/ui5-team-webc so the team can sign off explicitly.

💬 False positive? If you believe this PR doesn't actually change the public API (e.g. only internal refactoring, or an entry the detector mis-attributed), please reply on this thread — your feedback helps us improve the detection during this trial run.

Posted automatically by the Dev Close Notice workflow.

…avState interface

Replace the index-based ToolbarMovementInfo interface with a boolean
ToolbarArrowNavState { atLeftEnd, atRightEnd }. Items now report
whether they can absorb a keypress rather than exposing their internal
item count and cursor position.

- ToolbarItemBase/Item: remove ToolbarMovementInfo, ToolbarMovementEnabler,
  moveWithinToolbarItem. Add getArrowNavState() returning
  { atLeftEnd, atRightEnd } (delegates to single child if present,
  falls back to multi-child position check). Remove dead no-op branch
  in focusForToolbarNavigation().
- Input/TextArea: getToolbarMovementInfo ? getArrowNavState, returning
  { atLeftEnd: caret===0, atRightEnd: caret>=len }.
- Toolbar: _applyRovingTabIndex sets all items to tabIndex=0 (Tab exits
  naturally). _setCurrentItem only tracks _lastFocusedItem, no tabIndex
  manipulation. Arrow nav guards via getArrowNavState(). Home/End also
  guarded so caret-aware items (Input, TextArea) can handle them
  internally. _moveToNext/_moveToPrev clamped at boundaries, no wrap.
- Tests: add no-wrap boundary, Tab navigation, Input caret-aware nav,
  and Home key non-interception tests. Fix ToolbarSelect tests using
  incorrect "click" event name and jQuery .get(0) anti-pattern.
Comment thread packages/main/src/Toolbar.ts Outdated
Comment thread packages/main/src/ToolbarItem.ts
Comment thread packages/main/src/ToolbarItemBase.ts Outdated
…ocus model

Addresses the three review comments on the WAI-ARIA toolbar navigation and
applies follow-up hardening, without changing the established
arrow/Tab/Home/End behaviour.

Review comments
- Remove the roving tabindex layer entirely (arrow nav uses composedPath /
  getActiveElement / _lastFocusedItem + .focus(), never tabindex; the forced
  tabindex also double-added a tab stop on composite items). Drops
  _applyRovingTabIndex, setToolbarForcedTabIndex, getToolbarFocusTarget, the
  forcedTabIndex property and its reset in addItemsAdditionalProperties.
- isInteractive now checks whether the resolved focus target is actually
  focusable, so text-only content (ui5-text, ui5-title) no longer counts as a
  nav stop or toward role="toolbar". Resolves through getFocusDomRef so
  focus-delegating hosts (ui5-date-picker) stay navigable, and keeps href-less
  anchors that opt in via an explicit tabindex (ui5-link as a button, e.g. the
  Breadcrumbs overflow arrow) so a self-overflowed Breadcrumbs item is not
  skipped.
- Remove dead code: the unused ToolbarArrowNavigation interface, the
  write-only _lastFocusedNavigationTarget, and the dead `?? true` in
  _getNavigableItems.

Correctness hardening
- Input/TextArea getArrowNavState: a non-collapsed selection is no longer a
  navigation boundary; Left/Right collapse the selection natively.
- Home/End inside a multi-child group jump to the first/last child instead of
  being no-ops.
- Reconcile _lastFocusedItem after reflow so Tab re-entry and arrow/Home/End
  don't silently restart from the first item.

Lifecycle / cleanup
- Attach focusin/keydown/close-overflow listeners once in onEnterDOM/onExitDOM
  instead of per render; cache composedPath and resolve overflowButtonDOM once.
- Extract _resolveChildFocusTarget(); remove `as unknown as` double-casts, the
  `(this as any)` access, and the non-null assertion in the Tab branch.

Tests (Toolbar.cy.tsx)
- Text-only items non-interactive; focus-delegating hosts interactive;
  self-overflowed Breadcrumbs stays navigable.
- Input range-selection stays in the input at a boundary; TextArea caret-aware
  navigation; RTL arrow direction; Home/End within a multi-child group.
- ToolbarSelect programmatic-selection test stabilised against light-DOM
  option / rendered value.
@plamenivanov91
plamenivanov91 requested review from MapTo0 and niyap July 17, 2026 08:20
@plamenivanov91

Copy link
Copy Markdown
Contributor Author

Hello, @MapTo0 , @niyap

Can somebody from your team review Input and TextArea files please?

Regards,
Plamen

@MapTo0 MapTo0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I do not understand the TA use case. Someone places a TA in the toolbar?

Implementation looks a bit like a magic. Can we implement an interface in the components that might be placed within a toolbar? something like IToolbarCompatible. Tomorrow if someone see this method, he/she would not know why its there at all.

Furthermore, considering all native inputs should be tackled you can also check other components: ComboBox, MultiComboBox, FileUploader (not sure), SearchField.

What about non ui5-webcomponents elements placed within a toolbar? Can users also mix components?

@plamenivanov91

Copy link
Copy Markdown
Contributor Author

Honestly, I do not understand the TA use case. Someone places a TA in the toolbar?

Implementation looks a bit like a magic. Can we implement an interface in the components that might be placed within a toolbar? something like IToolbarCompatible. Tomorrow if someone see this method, he/she would not know why its there at all.

Furthermore, considering all native inputs should be tackled you can also check other components: ComboBox, MultiComboBox, FileUploader (not sure), SearchField.

What about non ui5-webcomponents elements placed within a toolbar? Can users also mix components?

Hello there,

We've discussed this implementation approach on the architecture round and there is nothing magical about it.

We have three mechanisms used to decide whether or not we are on the first/last index inside a complex control nested in the ToolbarItem and they are all explained in the commit message.

ToolbarItem — three navigation paths
_itemNavigation introspection — components that use ItemNavigation internally (e.g. Breadcrumbs, SegmentedButton) are detected via duck-typing; currentIndex/itemCount derive the boundary state
getArrowNavState interface — explicit opt-in for components that expose caret/selection position as boundary info (e.g. Input, TextArea)
Multi-child fallback — ToolbarItems with more than one slotted child (radio/checkbox groups) are treated as a navigable group; Left/Right and Home/End move within the group, exiting only at its boundary

The adaptation of the rest of controls you mentioned is not a Toolbar responsibility. The ones we altered are for demonstration purposes. Also they cover most of our sample code.

For non ui5-webcomponents (like groups) placed within a ToolbarItem we have different mechanism as you can see in the code and in the commit message.

Regards,
Plamen Ivanov

@plamenivanov91
plamenivanov91 requested a review from MapTo0 July 17, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ui5-toolbar][A11y]: toolbar keyboard interaction does not align with WCAG requirement

4 participants